Type Constructors are a thing you are almost guaranteed to have seen and used before, whether you knew it or not. Using them as a pattern however, is where you can unlock a lot of power and safety with significantly less code.
I often refer to type constructors as just being boxes around data. Sometimes, it's really easy to get data out of the box, sometimes it isn't, but the purpose of the box is to make types of data easier to work with.
Using them is a matter of recognizing what each type constructor is a pattern for. Lets go over them!
## Array
This one is gonna feel obvious- lists! That's it. You're already used to using its [Monadic](https://github.com/hemanth/functional-programming-jargon#monad) functions:
- `Array.of(2) === [2]`
- `numberList.map((n) => n + 2)`
They make it easier to work with lists, which is why items like `Array.prototype.find`, `Array.prototype.filter`, or `Array.prototype.reduce` are so common.
## Promise
You've almost certainly worked with Promises. Anytime you're fetching data, there you go.
> Many other languages have a feature that's very similar to a Promise, called a "Future". In some languages they're exactly the same, in others, they have slight differences. But they both serve the same purpose!
Did you know that Promises are a type constructor? Think about it. When you're not using `async`/`await`, you always interact with the data they contain from inside the box:
```typescript
const userPromise = submitUserToBackend(userParams)
.then((res) => res.json())
.then((body) = body.data)
```
`.then` here is doing the same thing that `map` does: It lets you change the data inside the Promise.
Furthermore, it even has functionality to allow you to specifically handle errors, quietly turning it into a `Promise<Either>`, kinda!
```typescript
userPromise.catch((error) => {
return error
})
```
Now, the type of `userPromise` is `Promise<User | Error>`. It's either a `User` or the `Error`.
## Either
That single `|` on the type above is the clue that the type under it might be suitable for another type constructor, `Either`. It's often used as a convenient way to handle errors without using `try {} catch {}`, which when nested becomes harder and harder to follow, not to mention verbose. For example, you could refactor the above `userPromise` code like this:
```typescript
import * as E from 'fp-ts/Either'
const userPromise = submitUserToBackend(userParams)
.then((res) => res.json())
.then((body) => body.data)
.then(E.right)
.catch(E.left)
```
Now, the type of `userPromise` is `Promise<Either<Error, User>>`. Having the data inside housed in an `Either` makes it easier for us to standardize how we react in these cases- if in the previous example, we wanted to determine it was an error or a user, we'd have to check if `(await result) instanceof Error`, and if neither side was an instance of a class, we'd have to check if it had a certain property, like `id` for user or `message` for the error.
Except even then, what if we put `id`s on errors? Now you've gotta refactor everything. Using `Either` makes it unambiguous as to what's going on, and additionally makes it impossible to make that mistake.
## Option
Personally, I've found this less useful in Typescript, as the way Typescript handles `undefined` and `null` is very close to how `Option`s are handled: It forces you to handle whether the data is present or not.
As a result, by itself, `Option` isn't as useful as one would hope. But, its helper functions do make it easier to work with when you're in a `pipe`, where otherwise you'd have to awkwardly do something like this:
```typescript
const getUser = (userId: string) => pipe(
getUserFromLocalCache(userId),
(user) => {
if (user === undefined) {
return getUserFromDisk(userId)
} else {
return user
}
}
)
```
The alternative implementation, using `Option`:
```typescript
import * as O from 'fp-ts/Option'
const getUser = (userId: string) => pipe(
getUserFromLocalCache(userId),
O.getOrElse(() => getUserFromDisk(userId))
)
```
Why use `Option` when we have `Either`? Because sometimes, there isn't an alternative value to what we're looking for. We either have it or we don't, and if we don't, it may not be a mistake. Furthermore, an `Either` says there is usefulness in the data on the `Left` side- in the error case, an error message or stack trace. What if those just aren't useful here?
## Task, TaskEither, TaskOption